<?php

namespace extend\bridgecli\client;

use Swoole\Coroutine;
use Swoole\Event;
use Swoole\Server;

class Socks5Client
{
    /**
     * 连接管理
     * @var \extend\bridgecli\BridgeProxy
     */
    protected $manage = null;
    /**
     * 地址
     * @var array
     */
    public $addr = [];
    /**
     * @var Coroutine\Socket
     */
    public $socket = null; // 代理最终的目标连接


    public $lastTime = 0; // 最后交互时间

    public function __construct(mixed $manage)
    {
        $this->manage = $manage;
        $this->lastTime = time();
    }

    /**
     * 连接消息
     * @param \Swoole\Server $server
     * @param int $tofd
     * @param mixed $data
     * @return void
     */
    public function connect(mixed $server, int $tofd, mixed $data)
    {
        $this->addr = $data;
        $this->addr['tofd'] = $tofd;
        list($ip, $type, $domain) = $this->manage->gethostbyname($data['host']);
        $this->addr['target_ip'] = $ip;
        $this->addr['target_port'] = $data['port'];
        $sockType = ($data['protocol'] == 1) ? SOCK_STREAM : SOCK_DGRAM;
        $this->addr['target_type'] = $sockType;
        $socket = new Coroutine\Socket($domain, $sockType);
        $this->socket = $socket;
        if($sockType == SOCK_STREAM){
            if(!$this->socket->connect($ip, $data['port'], 5)){
                $server->send($tofd, "连接失败", 'close');
                return false;
            }
            $server->send($this->addr['tofd'], $this->pack(0)); // 回复连接成功消息
            $this->wait($server);
            return true;
        }else{
            $server->send($this->addr['tofd'], $this->pack(0)); // 回复连接成功消息
            $this->wait($server);
            return true;
        }
    }

    public function send(mixed $server, int $tofd, string $data)
    {
        $data = base64_decode($data);
        $this->lastTime = time();
        if($this->addr['target_type'] == SOCK_STREAM){
            $this->socket->send($data);
        }else{
            $this->socket->sendto($this->addr['target_ip'], $this->addr['target_port'], $data);
        }
    }

    /**
     * 销毁掉
     * @param mixed $server
     * @param int $tofd
     * @return void
     */
    public function close(mixed $server, int $tofd)
    {
        if($this->socket){
            Event::del($this->socket);
            $this->socket->close();
        }
    }

    /**
     * @param Server $server
     * @return void
     */
    private function wait(mixed $server)
    {
        Event::add($this->socket, function (Coroutine\Socket $socket) use ($server){
            if($this->addr['target_type'] == SOCK_STREAM){
                $data = $socket->recv(65535);
            }else{
                $peer = null;
                $data = $socket->recvfrom($peer);
            }

            if (strlen($data) > 0) {
                Event::del($socket); // 先销毁事件
                $this->lastTime = time();
                $server->send($this->addr['tofd'], $data);
                $this->wait($server);
            }else{
                // 如果不是超时就应该断开
                if($socket->errCode != SOCKET_ETIMEDOUT || $data === ""){
                    $this->lastTime = 0;
                    Event::del($socket);
                    $socket->close();
                    $server->send($this->addr['tofd'], '连接异常断开', 'close');
                    unset($this->manage->httpProxyClients[$this->addr['tofd']]);
                }
            }
        });
    }

    /**
     * 生存报文格式数据
     * @param int $rep 0x00 成功，0x01常规 SOCKS 服务故障，0x02 规则不允许的连接，0x03 网络不可达，0x04 主机无法访问，0x05 拒绝连接，0x06 连接超时，0x07 不支持的命令，0x08 不支持的地址类型
     * @param int $atvp 地址类型 0x01 表明地址为（DST.ADDR字段）IPV4 地址,长度为4字节， 0x03域名，表明地址为域名，第一个字节用作域名的长度标识， 0x04 表明地址为IPV6 地址，长度为16字节
     * @param string $bndAddr 要访问的目标服务器的地址或域名
     * @param int $bndPort 目标地址对应的端口号
     * @return void
     */
    public function pack(int $rep = 0, int $atvp = 0x01, string $bndAddr = '0.0.0.0', int $bndPort = 0)
    {
        $data = "";
        $data .= "\x05"; //VERSION SOCKS协议版本，固定0x05
        $data .= chr($rep);
        $data .= chr(0); //RSV 保留字段
        $data .= chr($atvp);

        switch ($atvp) {
            // 如果是ipV4
            case 0x01:
                $tmp = explode('.', $bndAddr);
                foreach ($tmp as $block) {
                    $data .= chr((int)$block);
                }
                break;
            // 如果是域名地址
            case 0x03:
                $host_len = strlen($bndAddr);
                $data .= chr($host_len);
                $data .= $bndAddr;
                break;
        }

        $data .= pack("n", $bndPort);
        return $data;
    }
}